

/**
 * Copyright (c) 2006-2008, Julien PORTALIER
 * 
 * Licensed under The LGPL License
 * Redistributions of files must retain the above copyright notice.
 * 
 * @copyright  Copyright (c) 2006-2008, Julien PORTALIER
 * @link       http://feather-cm.google.com/
 * @license    http://www.opensource.org/licenses/lgpl-license.php The LGPL License
 */

var Autocompleter = {};

if (!Array.prototype.test) {
	Array.prototype.test = Array.prototype.contains;
}

/**
 * Autocompletes a single token.
 * 
 * @param Input  input Field to autocomplete;
 * @param String url   URL to query;
 * @param Object options:
 *   minChars : minimum chars before autocompletion (defaults to 1);
 *   timeout  : time to wait before autocompletion (defaults to 500ms);
 *   paramName: parameter name (defaults to no parameter, the token is simply added to the URL);
 *   className: CSS class for choices' list;
 *   specialKeys: some keys that musn't react when pressed.
 */
Autocompleter.Base = new Class(
{
	initialize: function(input, url, options)
	{
		this.url     = url;
		this.options = {
			minChars:    1,
			timeout:     500,
			paramName:   null,
			className:   'autocomplete',
			specialKeys: ['down', 'up', 'left', 'right', 'enter', 'tab', 'delete', 'backspace'],
			callback:    function() {},
			positionReferer: null
		};
		this.setOptions(options);
		
		this.choices  = [];
		this.elements = {};
		this.elements.input   = $(input);
		this.elements.choices = new Element('ul', {
			'class': this.options.className,
			'styles': {'display':  'none'}
		});
		
		// events
		this.elements.input.addEvent('keypress',    this.onKeypress.bindWithEvent(this));
		this.elements.input.addEvent('blur',        this.onCancel.bindWithEvent(this));
		this.elements.choices.addEvent('mouseover', this.onMouseOver.bindWithEvent(this));
		this.elements.choices.addEvent('mousedown', this.onChoice.bindWithEvent(this));
	},
	
	// tokens
	
	getToken: function()
	{
		return this.elements.input.value;
	},
	
	setToken: function(token)
	{
		this.elements.input.value = token;
	},
	
	// events
	
	onCancel: function()
	{
		this.elements.choices.setStyle('display', 'none');
//		this.choices = [];
	},
	
	resetChoices: function()
	{
		this.choices = [];
	},
	
	onKeypress: function(evt)
	{
		switch(evt.key)
		{
			case 'down':  this.markChoice(Math.min(this.index + 1, this.choices.length - 1)); evt.stop(); return;
			case 'up':    this.markChoice(Math.max(this.index - 1, 0));                   evt.stop(); return;
			case 'enter': this.onChoice(); evt.stop(); return;
			case 'esc':   this.onCancel(); evt.stop(); this.doSearch = false; return;
			case ',':     this.onCancel(); this.doSearch = false; return;
			default:      this.doSearch = true;
		}
		if (this.doSearch && !this.options.specialKeys.test(evt.key))
		{
			$clear(this.timer);
			this.timer = this.getChoices.delay(this.options.timeout, this);
		}
	},
	
	onMouseOver: function(evt)
	{
		this.markChoice(evt.target.index);
	},
	
	// requests
	
	getChoices: function()
	{
		this.token = this.getToken();
		if (this.token.length >= this.options.minChars && !(/^\s*$/.test(this.token)) && !this.ajax)
		{
			if (this.options.paramName)
				this.url += '?' + this.options.paramName + '=';
			this.ajax = new Ajax(this.url + encodeURIComponent(this.token),
			{
				'method': 'get',
				'onComplete': this.updateChoices.bind(this)
			}).request();
		}
	},
	
	updateChoices: function(html)
	{
		this.choices = [];
		
		// IE6 can't change an element's innerHTML if the element is added to the DOM (?!)
		// we thus remove it, update the innerHTML, and finally reinject it.
		if (this.elements.choices.parentNode)
			this.elements.choices.remove();
		
		this.elements.choices.setHTML(html);
		var choices = $ES('li', this.elements.choices);
		if (choices.length > 0)
		{
			this.positionChoices();
			for (var index=0; index<choices.length; index++)
			{
				li        = $(choices[index]);
				li.index  = index;
				var token = li.innerHTML;
				this.choices.push(token);
				li.setHTML(token.replace(new RegExp('^(' + this.token + ')', 'i'), '<span>$1</span>'));
			}
			this.markChoice(0);
//			this.elements.choices.injectAfter(this.elements.input);
			document.body.appendChild(this.elements.choices);
		}
		this.ajax = null;
	},
	
	positionChoices: function()
	{
		var pos = this.elements.input.getCoordinates();
		this.elements.choices.setStyles({
			'display':  'block',
			'position': 'absolute',
			'width':    pos.width + 'px',
			'top':      pos.top + pos.height + 'px',
			'left':     pos.left + 'px',
			'z-index':   1000,
			'zoom': 1
		});
	},
	
	// choices
	
	markChoice: function(index)
	{
		this.index  = index || 0;
		var choices = $ES('li', this.elements.choices);
		var choice  = choices[this.index];
		
		if (choice && !choice.hasClass('selected'))
		{
			choices.removeClass('selected');
			choice.addClass('selected');
		}
	},
	
	onChoice: function()
	{
		this.setToken(this.choices[this.index]);
		this.onCancel();
		this.options.callback();
	}
});
Autocompleter.Base.implement(new Options());

/**
 * Autocompletes multiple tokens.
 * 
 * Option: tokenMark (defaults to ',')
 */
Autocompleter.Multi = Autocompleter.Base.extend(
{
	initialize: function(input, url, options)
	{
		this.parent(input, url, options);
		if (!this.options.tokenMark)
			this.options.tokenMark = ',';
		this.tokens = [];
		this.tokenIndex = 0;
	},
	
	getToken: function()
	{
		var token = this.elements.input.value;
		this.tokenIndex = null;
		
		// multiple tokens?
		if (token.indexOf(this.options.tokenMark) > -1)
		{
			var pos = token.length;
			
			// gets caret position
			if (document.selection)
			{
				// IE
				this.elements.input.focus ();
				var sel = document.selection.createRange ();
				sel.moveStart('character', -token.length);
				pos = sel.text.length;
			}
			else if (this.elements.input.selectionStart || this.elements.input.selectionStart == '0')
			{
				// firefox, opera (& safari?)
				pos = this.elements.input.selectionStart;
			}
			
			// gets currently edited token
			this.tokenIndex = token.substr(0, pos).split(this.options.tokenMark).length - 1;
			token = token.split(this.options.tokenMark)[this.tokenIndex];
		}
		
		return token.trim();
	},
	
	setToken: function(token)
	{
		var value = this.elements.input.value;
		var space = this.options.tokenMark + ' ';
		
		// multiple tokens?
		if (value.indexOf(this.options.tokenMark) > -1)
		{
			// gets tokens
			var tokens = value.split(this.options.tokenMark);
			
			// updates edited token
			tokens[this.tokenIndex] = token;
			
			value = '';
			for (var i = 0; i < tokens.length; i++)
			{
				tokens[i] = tokens[i].trim();
				if (tokens[i] != '')
					value += tokens[i] + space;
			}
			
			this.elements.input.value = value;
		}
		else
		{
			// single token
			this.elements.input.value = token + space;
		}
	}
});
